using System;

using Storyteller.Framework;
using Storyteller.Framework.Math;

namespace Storyteller.Framework.Graphics
{
	public class SpriteBatch
	{
		public SpriteBatch(SpriteShader defaultShader)
			: this(1000, defaultShader)
		{
		}
		
		public SpriteBatch(int maximumSprites, SpriteShader defaultShader)
		{
			this.defaultShader = defaultShader;
			this.graphicsContext = Game.Instance.GraphicsContext;
			
			this.maximumSprites = maximumSprites;
			
			vertices = new float[maximumSprites * 9 * 6];
			vertexBufferPool = new VertexBufferPool(10, maximumSprites * 6, Sce.Pss.Core.Graphics.VertexFormat.Float3,
				Sce.Pss.Core.Graphics.VertexFormat.Float2, Sce.Pss.Core.Graphics.VertexFormat.Float4);
			
			Game.Instance.Register(this);
		}		
		
		internal void BeginFrame()
		{
			vertexBufferPool.Reset();
		}
		
		public void Begin()
		{
			Begin(BlendMode.Alpha, defaultShader, Matrix4.Identity);
		}
		
		public void Begin(BlendMode blendState)
		{
			Begin(blendMode, defaultShader, Matrix4.Identity);
		}

		public void Begin(BlendMode blendMode, Matrix4 batchTransform)
		{
			Begin(blendMode, defaultShader, batchTransform);
		}

		public void Begin(BlendMode blendMode, SpriteShader spriteShader)
		{
			Begin(blendMode, spriteShader, Matrix4.Identity);
		}

		public void Begin(BlendMode blendMode, SpriteShader spriteShader, Matrix4 batchTransform)
		{
			if(insideBegin)
				throw new InvalidOperationException("You must call SpriteBatch.End before calling SpriteBatch.Begin again");
			
			insideBegin = true;
			
			this.blendMode = blendMode;
			this.batchTransform = batchTransform;
			currentShader = spriteShader;
			currentTexture = null;
			spriteCount = 0;
			
			graphicsContext.Enable(Sce.Pss.Core.Graphics.EnableMode.CullFace);
			graphicsContext.SetCullFace(Sce.Pss.Core.Graphics.CullFaceMode.None, Sce.Pss.Core.Graphics.CullFaceDirection.Ccw);
			
			graphicsContext.Enable(Sce.Pss.Core.Graphics.EnableMode.Blend);
			graphicsContext.SetBlendFunc(blendMode.BlendFunc);
			
			vertexBuffer = vertexBufferPool.GetBuffer();
		}
		
		public void End()
		{
			if(!insideBegin)
				throw new InvalidOperationException("SpriteBatch.End called without a cooresponding SpriteBatch.Begin");

			insideBegin = false;
			
			if(spriteCount > 0)
				DrawSprites();
			
			currentTexture = null;
			spriteCount = 0;
		}
		
		public void Draw(Texture texture, TextureRegion textureRegion, Vector2 position, Color color)
		{
			Draw(texture, position, textureRegion.Bounds, color, 0, textureRegion.OriginCenter, Vector2.One, textureRegion.Rotated ? TextureEffects.RotatePackedUVs : TextureEffects.None, Matrix4.Identity);
		}

		public void Draw(Texture texture, TextureRegion textureRegion, Vector2 position, Color color, float rotation, Vector2 scale, TextureEffects effects, Matrix4 transformMatrix)
		{
			Draw(texture, position, textureRegion.Bounds, color, rotation, textureRegion.OriginCenter, scale, effects | (textureRegion.Rotated ? TextureEffects.RotatePackedUVs : TextureEffects.None), transformMatrix);
		}

		public void Draw(Texture texture, TextureRegion textureRegion, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, TextureEffects effects, Matrix4 transformMatrix)
		{
			origin.X = MathHelper.Lerp(textureRegion.OriginTopLeft.X, textureRegion.OriginBottomRight.X, origin.X);
			origin.Y = MathHelper.Lerp(textureRegion.OriginTopLeft.Y, textureRegion.OriginBottomRight.Y, origin.Y);
			Draw(texture, position, textureRegion.Bounds, color, rotation, origin, scale, effects | (textureRegion.Rotated ? TextureEffects.RotatePackedUVs : TextureEffects.None), transformMatrix);
		}

		public void Draw(Texture texture, Vector2 position, Color color)
		{
			Draw(texture, position, null, color, 0, Vector2.Zero, Vector2.One, TextureEffects.None, Matrix4.Identity);
		}

		public void Draw(Texture texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, TextureEffects effects, Matrix4 transformMatrix)
		{
			if (spriteCount == maximumSprites || (currentTexture != null && !currentTexture.Equals(texture)))
				DrawSprites();
			
			if(texture is RenderTexture)
				effects |= TextureEffects.FlipVertically;

			currentTexture = texture;

			position -= new Vector2(0.5f);

			Rectangle textureRect = new Rectangle(0, 0, texture.Width, texture.Height);
			Rectangle rectangle;

			if (sourceRectangle.HasValue)
				rectangle = sourceRectangle.Value;
			else
				rectangle = textureRect;

			int width = rectangle.Width;
			int height = rectangle.Height;

			int textureWidth = texture.Width;
			int textureHeight = texture.Height;

			Vector2 uvTL, uvTR, uvBL, uvBR;
			Vector2 temp;

			if ((effects & TextureEffects.RotatePackedUVs) != TextureEffects.None)
			{
				uvTL = new Vector2((float)(rectangle.X + rectangle.Height) / textureWidth,
					(float)rectangle.Y / textureHeight);
				uvTR = new Vector2((float)(rectangle.X + rectangle.Height) / textureWidth,
					(float)(rectangle.Y + rectangle.Width) / textureHeight);
				uvBL = new Vector2((float)rectangle.X / textureWidth,
					(float)rectangle.Y / textureHeight);
				uvBR = new Vector2((float)rectangle.X / textureWidth,
					(float)(rectangle.Y + rectangle.Width) / textureHeight);
			}
			else
			{
				uvTL = new Vector2(rectangle.X / (float)textureWidth, rectangle.Y / (float)textureHeight);
				uvTR = new Vector2((rectangle.X + rectangle.Width) / (float)textureWidth, rectangle.Y / (float)textureHeight);
				uvBL = new Vector2(rectangle.X / (float)textureWidth, (rectangle.Y + rectangle.Height) / (float)textureHeight);
				uvBR = new Vector2((rectangle.X + rectangle.Width) / (float)textureWidth, (rectangle.Y + rectangle.Height) / (float)textureHeight);
			}

			if ((effects & TextureEffects.RotateClockwise90) != TextureEffects.None)
			{
				temp = uvTL;
				uvTL = uvBL;
				uvBL = uvBR;
				uvBR = uvTR;
				uvTR = temp;
			}
			if ((effects & TextureEffects.FlipVertically) != TextureEffects.None)
			{
				temp = uvTL;
				uvTL = uvBL;
				uvBL = temp;
				temp = uvTR;
				uvTR = uvBR;
				uvBR = temp;
			}
			if ((effects & TextureEffects.FlipHorizontally) != TextureEffects.None)
			{
				temp = uvTL;
				uvTL = uvTR;
				uvTR = temp;
				temp = uvBL;
				uvBL = uvBR;
				uvBR = temp;
			}

			Vector3 v;
			Matrix4 transform = Matrix4.Scale(scale.X, scale.Y, 1.0f) *
				Matrix4.CreateRotationZ(rotation) *
				Matrix4.CreateTranslation(position.X, position.Y, 0) *
				transformMatrix * batchTransform;

			v.X = -width * origin.X;
			v.Y = -height * origin.Y;
			v.Z = 0;
			v = Vector3.Transform(v, transform);
			
			int vertexOffset = spriteCount * 9 * 6;
			vertices[vertexOffset + 0] = v.X;
			vertices[vertexOffset + 1] = v.Y;
			vertices[vertexOffset + 2] = 0;
			vertices[vertexOffset + 3] = uvTL.X;
			vertices[vertexOffset + 4] = uvTL.Y;
			vertices[vertexOffset + 5] = color.R;
			vertices[vertexOffset + 6] = color.G;
			vertices[vertexOffset + 7] = color.B;
			vertices[vertexOffset + 8] = color.A;
			vertexOffset += 9;
						
			v.X = width * (1 - origin.X);
			v.Y = -height * origin.Y;
			v.Z = 0;
			v = Vector3.Transform(v, transform);
			
			vertices[vertexOffset + 0] = vertices[vertexOffset + 27] = v.X;
			vertices[vertexOffset + 1] = vertices[vertexOffset + 28] = v.Y;
			vertices[vertexOffset + 2] = vertices[vertexOffset + 29] = 0;
			vertices[vertexOffset + 3] = vertices[vertexOffset + 30] = uvTR.X;
			vertices[vertexOffset + 4] = vertices[vertexOffset + 31] = uvTR.Y;
			vertices[vertexOffset + 5] = vertices[vertexOffset + 32] = color.R;
			vertices[vertexOffset + 6] = vertices[vertexOffset + 33] = color.G;
			vertices[vertexOffset + 7] = vertices[vertexOffset + 34] = color.B;
			vertices[vertexOffset + 8] = vertices[vertexOffset + 35] = color.A;
			vertexOffset += 9;

			v.X = -width * origin.X;
			v.Y = height * (1 - origin.Y);
			v.Z = 0;
			v = Vector3.Transform(v, transform);

			vertices[vertexOffset + 0] = vertices[vertexOffset + 9] = v.X;
			vertices[vertexOffset + 1] = vertices[vertexOffset + 10] = v.Y;
			vertices[vertexOffset + 2] = vertices[vertexOffset + 11] = 0;
			vertices[vertexOffset + 3] = vertices[vertexOffset + 12] = uvBL.X;
			vertices[vertexOffset + 4] = vertices[vertexOffset + 13] = uvBL.Y;
			vertices[vertexOffset + 5] = vertices[vertexOffset + 14] = color.R;
			vertices[vertexOffset + 6] = vertices[vertexOffset + 15] = color.G;
			vertices[vertexOffset + 7] = vertices[vertexOffset + 16] = color.B;
			vertices[vertexOffset + 8] = vertices[vertexOffset + 17] = color.A;
			vertexOffset += 27;			

			v.X = width * (1 - origin.X);
			v.Y = height * (1 - origin.Y);
			v.Z = 0;
			v = Vector3.Transform(v, transform);
			
			vertices[vertexOffset + 0] = v.X;
			vertices[vertexOffset + 1] = v.Y;
			vertices[vertexOffset + 2] = 0;
			vertices[vertexOffset + 3] = uvBR.X;
			vertices[vertexOffset + 4] = uvBR.Y;
			vertices[vertexOffset + 5] = 1;
			vertices[vertexOffset + 6] = 1;
			vertices[vertexOffset + 7] = 1;
			vertices[vertexOffset + 8] = 1;
			
			spriteCount++;
		}
		
		public void DrawString(Font font, string s, Vector2 position, Color color, float rotation, Vector2 scale, Matrix4 transformMatrix)
		{
			Texture texture = font.Texture;

			if (spriteCount == maximumSprites || (currentTexture != null && !currentTexture.Equals(texture)))
				DrawSprites();

			currentTexture = texture;

			Vector2 origin = Vector2.Zero;

			float x = 0, y = 0;

			for (int i = 0; i < s.Length; i++)
			{
				CharacterData cd = font[(short)s[i]];

				if (cd.Width == 0 || cd.Height == 0)
				{
					x += cd.AdvanceX;
					continue;
				}

				if (spriteCount == maximumSprites)
					DrawSprites();

				Rectangle sourceRectangle = cd.TextureRegion.Bounds;

				int width = sourceRectangle.Width;
				int height = sourceRectangle.Height;

				int textureWidth = texture.Width;
				int textureHeight = texture.Height;

				Vector2 uvTL;
				Vector2 uvTR;
				Vector2 uvBL;
				Vector2 uvBR;

				if (cd.TextureRegion.Rotated)
				{
					uvTL = new Vector2((float)(sourceRectangle.X + sourceRectangle.Height) / textureWidth,
						(float)sourceRectangle.Y / textureHeight);
					uvTR = new Vector2((float)(sourceRectangle.X + sourceRectangle.Height) / textureWidth,
						(float)(sourceRectangle.Y + sourceRectangle.Width) / textureHeight);
					uvBL = new Vector2((float)sourceRectangle.X / textureWidth,
						(float)sourceRectangle.Y / textureHeight);
					uvBR = new Vector2((float)sourceRectangle.X / textureWidth,
						(float)(sourceRectangle.Y + sourceRectangle.Width) / textureHeight);
				}
				else
				{
					uvTL = new Vector2(sourceRectangle.X / (float)textureWidth, sourceRectangle.Y / (float)textureHeight);
					uvTR = new Vector2((sourceRectangle.X + sourceRectangle.Width) / (float)textureWidth, sourceRectangle.Y / (float)textureHeight);
					uvBL = new Vector2(sourceRectangle.X / (float)textureWidth, (sourceRectangle.Y + sourceRectangle.Height) / (float)textureHeight);
					uvBR = new Vector2((sourceRectangle.X + sourceRectangle.Width) / (float)textureWidth, (sourceRectangle.Y + sourceRectangle.Height) / (float)textureHeight);
				}

				Vector3 v;
				Matrix4 transform = Matrix4.Scale(scale.X, scale.Y, 1) *
					Matrix4.CreateRotationZ(rotation) *
					Matrix4.CreateTranslation(position.X + x + cd.OffsetX, position.Y + y - cd.OffsetY, 0) *
					transformMatrix * batchTransform;

				v.X = -width * origin.X;
				v.Y = -height * origin.Y;
				v.Z = 0;
				v = Vector3.Transform(v, transform);

				int vertexOffset = spriteCount * 9 * 6;
				vertices[vertexOffset + 0] = v.X;
				vertices[vertexOffset + 1] = v.Y;
				vertices[vertexOffset + 2] = 0;
				vertices[vertexOffset + 3] = uvTL.X;
				vertices[vertexOffset + 4] = uvTL.Y;
				vertices[vertexOffset + 5] = color.R;
				vertices[vertexOffset + 6] = color.G;
				vertices[vertexOffset + 7] = color.B;
				vertices[vertexOffset + 8] = color.A;
				vertexOffset += 9;
							
				v.X = width * (1 - origin.X);
				v.Y = -height * origin.Y;
				v.Z = 0;
				v = Vector3.Transform(v, transform);
				
				vertices[vertexOffset + 0] = vertices[vertexOffset + 27] = v.X;
				vertices[vertexOffset + 1] = vertices[vertexOffset + 28] = v.Y;
				vertices[vertexOffset + 2] = vertices[vertexOffset + 29] = 0;
				vertices[vertexOffset + 3] = vertices[vertexOffset + 30] = uvTR.X;
				vertices[vertexOffset + 4] = vertices[vertexOffset + 31] = uvTR.Y;
				vertices[vertexOffset + 5] = vertices[vertexOffset + 32] = color.R;
				vertices[vertexOffset + 6] = vertices[vertexOffset + 33] = color.G;
				vertices[vertexOffset + 7] = vertices[vertexOffset + 34] = color.B;
				vertices[vertexOffset + 8] = vertices[vertexOffset + 35] = color.A;
				vertexOffset += 9;
	
				v.X = -width * origin.X;
				v.Y = height * (1 - origin.Y);
				v.Z = 0;
				v = Vector3.Transform(v, transform);
	
				vertices[vertexOffset + 0] = vertices[vertexOffset + 9] = v.X;
				vertices[vertexOffset + 1] = vertices[vertexOffset + 10] = v.Y;
				vertices[vertexOffset + 2] = vertices[vertexOffset + 11] = 0;
				vertices[vertexOffset + 3] = vertices[vertexOffset + 12] = uvBL.X;
				vertices[vertexOffset + 4] = vertices[vertexOffset + 13] = uvBL.Y;
				vertices[vertexOffset + 5] = vertices[vertexOffset + 14] = color.R;
				vertices[vertexOffset + 6] = vertices[vertexOffset + 15] = color.G;
				vertices[vertexOffset + 7] = vertices[vertexOffset + 16] = color.B;
				vertices[vertexOffset + 8] = vertices[vertexOffset + 17] = color.A;
				vertexOffset += 27;			
	
				v.X = width * (1 - origin.X);
				v.Y = height * (1 - origin.Y);
				v.Z = 0;
				v = Vector3.Transform(v, transform);
				
				vertices[vertexOffset + 0] = v.X;
				vertices[vertexOffset + 1] = v.Y;
				vertices[vertexOffset + 2] = 0;
				vertices[vertexOffset + 3] = uvBR.X;
				vertices[vertexOffset + 4] = uvBR.Y;
				vertices[vertexOffset + 5] = 1;
				vertices[vertexOffset + 6] = 1;
				vertices[vertexOffset + 7] = 1;
				vertices[vertexOffset + 8] = 1;
				
				spriteCount++;

				x += cd.AdvanceX;
			}
		}
		
		private Sce.Pss.Core.Matrix4 ConvertMatrix(Matrix4 matrix)
		{
			return new Sce.Pss.Core.Matrix4(matrix.M11, matrix.M12, matrix.M13, matrix.M14,
				matrix.M21, matrix.M22, matrix.M23, matrix.M24,
				matrix.M31, matrix.M32, matrix.M33, matrix.M34,
				matrix.M41, matrix.M42, matrix.M43, matrix.M44);
		}
		
		void DrawSprites()
		{
			graphicsContext.SetVertexBuffer(0, vertexBuffer);

			vertexBuffer.SetVertices(0, vertices, 0, sizeof(float) * 9);
			vertexBuffer.SetVertices(1, vertices, sizeof(float) * 3, sizeof(float) * 9);
			vertexBuffer.SetVertices(2, vertices, sizeof(float) * 5, sizeof(float) * 9);
			
			Sce.Pss.Core.Imaging.ImageRect viewPort = graphicsContext.GetViewport();
			Sce.Pss.Core.Matrix4 m = ConvertMatrix(currentShader.GetWorldMatrix(viewPort.Width, viewPort.Height));
			
			graphicsContext.SetShaderProgram(currentShader.ShaderProgram);
			graphicsContext.SetTexture(0, currentTexture.Texture2D);
			currentShader.ShaderProgram.SetUniformValue(0, ref m);
			
			graphicsContext.DrawArrays(Sce.Pss.Core.Graphics.DrawMode.Triangles, 0, spriteCount * 6);

			spriteCount = 0;
			currentTexture = null;
		}
		
		SpriteShader defaultShader;
		Sce.Pss.Core.Graphics.GraphicsContext graphicsContext;

		bool insideBegin = false;

		Texture currentTexture = null;
		SpriteShader currentShader = null;

		BlendMode blendMode = BlendMode.Alpha;

		int maximumSprites;
		int spriteCount = 0;

		float[] vertices;
		Sce.Pss.Core.Graphics.VertexBuffer vertexBuffer;
		VertexBufferPool vertexBufferPool;

		Matrix4 batchTransform = Matrix4.Identity;
	}
}
